chore(deps): update dependency gitpython to v3.1.58 [security] - #11
Open
renovate[bot] wants to merge 1 commit into
Open
chore(deps): update dependency gitpython to v3.1.58 [security]#11renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/pypi-gitpython-vulnerability
branch
2 times, most recently
from
August 5, 2026 01:27
f8b5f17 to
c6439b0
Compare
renovate
Bot
force-pushed
the
renovate/pypi-gitpython-vulnerability
branch
from
August 7, 2026 17:05
c6439b0 to
9e93abd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
==3.1.55→==3.1.58GitPython: Arbitrary file truncation via git rev-list --output argument injection in unguarded Commit.count
GHSA-p538-c434-8v24
More information
Details
Summary
Commit.count()forwards**kwargsintorev_listwith nocheck_unsafe_optionsguard (the guard exists only in the siblingiter_items, commit.py:341).git rev-list --output=<path>opens and truncates the target file to 0 bytes before revision parsing, socount(output='/victim')destroys/blanks an arbitrary file.Root Cause
commit.py:290-291callsself.repo.git.rev_list(self.hexsha, **kwargs)with nocheck_unsafe_optionsand noallow_unsafe_optionsparameter. The siblingiter_items(commit.py:341) is guarded;countis not. This is a distinct, uncovered sink — GHSA-956x-8gvw-wg5v fixediter_commits/blame, notcount.Impact
Destroy/blank an arbitrary file at process privilege (integrity/availability). Reachability is key-control only (
countusesself.hexsha, not a user ref), and the write is a 0-byte truncation (no content control), so MEDIUM.Proof of Concept
Attack Chain
commit.count(output='/victim'). Guard: none. Bypass proof:iter_commits(output=)raises UnsafeOptionError;count(output=)does not — verified side-by-side.git rev-list <sha> --output=/victim-> file truncated to 0 bytes. Impact: destroy/blank arbitrary file.Bypass Evidence
Live-verified on HEAD (tag 3.1.53):
count(output=<victim>)truncated a pre-existing file to 0 bytes; guardediter_commits(output=)raised UnsafeOptionError. Same CNA-accepted "app forwards user options dict" model as GHSA-956x-8gvw-wg5v'sarchive(**kwargs). Uncovered sink, not a duplicate.Affected Versions
<= 3.1.53Suggested Fix
Add
check_unsafe_optionstoCommit.count(mirroringiter_items).Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unguarded git option forwarding in IndexFile.checkout() and TagReference.create() enables arbitrary file overwrite and arbitrary file read
GHSA-3f7w-8rr8-f37f
More information
Details
Target: gitpython-developers/GitPython
Tested: HEAD
07e80555(2026-07-25), latest release 3.1.55,git version 2.50.1Reported instances: 2 exploitable, from a sweep of 14 unguarded call sites
Summary
GitPython blocks dangerous git options through
Git.check_unsafe_options(), gated per method by anallow_unsafe_optionsparameter. That guard is applied per call site, so any API that forwards**kwargsinto a git command without calling it passes caller-controlled options straight to git.A mechanical sweep of every method that forwards
**kwargsinto a.git.<command>(...)call found 14 sites with no guard. Two reach a git option that takes a filesystem path:IndexFile.checkout()→git checkout-index--prefix=<path>TagReference.create()→git tag-F <file>/--file=<file>This is the same defect class already fixed in
Commit.count()(GHSA-p538-c434-8v24),Repo.archive()andGit.ls_remote()(GHSA-956x-8gvw-wg5v). Both instances below are still present at HEAD.Instance 1 —
IndexFile.checkout(): arbitrary file overwritegit/index/base.py:1210accepts**kwargsand forwards them with no guard:There is no
allow_unsafe_optionsparameter and nocheck_unsafe_options()call in the method.git checkout-indexaccepts--prefix=<string>, prepended to every output path. It is not confined to the working tree, so an absolute prefix writes tracked file contents anywhere the process can write, and-foverwrites what is already there.Reproduction
Observed (
poc/poc_checkout_index.py) — no exception raised, files land outside the repository:Overwrite of a pre-existing file (
poc/poc_ci_overwrite.py) — the victim file heldORIGINAL-DO-NOT-CLOBBER\nbefore the call:Why this rates High
Both halves of the write are attacker-influenced:
prefixkwarg.Commit a file named
authorized_keys,.bashrc,configorpost-checkout, choose the matching prefix (~/.ssh/,~/,.git/hooks/), and the write becomes code execution as the service account.For comparison within this project: GHSA-fjr4-x663-mwxc (arbitrary file overwrite via
git diff --output) is rated High, and GHSA-p538-c434-8v24 (arbitrary file truncation viagit rev-list --output) is rated Medium.--prefixsupplies full content control, so it sits at or above the former.Instance 2 —
TagReference.create(): arbitrary file readgit/refs/tag.py:88forwards**kwargsintogit tagwith no guard, and the signature advertises the passthrough:git tagaccepts-F <file>/--file=<file>, which reads the tag message from an arbitrary path. The annotated tag object stores that content and GitPython returns it to the caller viaTagReference.tag.message, so the file contents come back in-band.Reproduction
Observed (
poc/poc_tag_F.py), reading a canary file outside the repository:Impact is a read at the privileges of the process. I am not claiming code execution for this instance. The signing options (
-s,-u/--local-user) do invoke gpg from the same unguarded kwargs, but I did not develop that into command execution and make no claim about it.Sweep results — the other 12 sites
Reported so the fix can be scoped once rather than per report.
poc/sweep.pyreproduces this list.IndexFile.from_tree()read-tree--index-output=<path>looked reachable but is neutralised: GitPython appends its own--index-outputafter the caller's kwargs and git honours the last occurrence. Verified — victim file unchanged (poc/poc_readtree.py)IndexFile.remove()rm--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive foundIndexFile.move()mvHEAD.reset()resetHEAD.checkout()checkoutHead.delete(),RemoteReference.delete()branchRepo.merge_base()merge-baseRepo._get_untracked_files()statusRemote.set_url(),Remote.create(),Remote.update()remoteSuggested remediation
Immediate: add
allow_unsafe_options: bool = Falseto both methods and gateGit._option_candidates(args, kwargs)against new lists —unsafe_git_checkout_index_options = ["--prefix"](consider--temp) andunsafe_git_tag_options = ["--file", "-F"](consider-s,-u/--local-user,--cleanup) — matching the pattern used inRepo.archive()andCommit.count().Structural: this defect has now been fixed four times in four places (
Repo.archive(),Git.ls_remote(),Commit.count(), and the two here), because the guard is opt-in per method: every new**kwargs-forwarding API starts unguarded and stays that way until someone reports it. Enforcing the check centrally inGit._call_process()— each git invocation consults a per-command unsafe-option table unless the caller opts out — would make new call sites safe by default rather than by review, and would close the remaining sites in the table above at the same time.Disclosure
Reported privately via GitHub private vulnerability reporting.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Incomplete unsafe_git_archive_options denylist omits --add-file / --add-virtual-file, enabling arbitrary file read via Repo.archive()
GHSA-539m-9xh6-q6rr
More information
Details
Target: gitpython-developers/GitPython
Tested: HEAD
07e80555(2026-07-25), latest release 3.1.55,git version 2.50.1Summary
Repo.archive()does call the option guard, so this is not a missing-guard report. The guard is present and working; the denylist it consults is incomplete.The comment on
--outputstates the protected class in the project's own words: an option that lets the caller name a filesystem path is unsafe.--outputis blocked because it writes to a caller-chosen path.git archivealso accepts--add-file=<path>and--add-virtual-file=<path:content>(both present in current git; verified againstgit version 2.50.1).--add-filereads a caller-chosen path — including an absolute path outside the repository — and places the bytes into the archive the caller receives. Neither option is in the list, and no other layer references them:Net effect: the guard blocks arbitrary file write at this sink while permitting arbitrary file read at the same sink.
Reachability proof (verified at the sink)
poc/poc_addfile.pyat HEAD07e80555. The PoC creates its own out-of-tree canary, so it runs from a clean machine:The three blocked lines are the control: they prove the guard is active on this call path, so the fourth result is a gap in list membership rather than a guard that never ran.
Minimal reproduction:
The canary is untracked and lives outside the repository; its contents are recovered from the returned archive and asserted byte-for-byte against the on-disk file. The option is rendered by
transform_kwargsinto--add-file=<path>and reachesgit archiveunmodified.Direct precedent
GHSA-6p8h-3wgx-97gf(High, published 2026-07-22) is the same defect on the sibling list: "Incompleteunsafe_git_clone_optionsdenylist omits--template" — an option absent from one of these denylists, reachable under the same caller-controlled-options precondition, accepted and fixed by adding it.git logshows the archive list itself has already been extended reactively once, in701ce32f(fix: Guard unsafe git command options, GHSA-956x-8gvw-wg5v), and the--templateomission was then fixed separately inffcb5359.--add-virtual-fileis the same gap pointing the other way--add-virtual-file=<path:content>lets the caller inject attacker-chosen content under an attacker-chosen name into an archive that downstream consumers will reasonably treat as repository-derived.Suggested remediation
Repo.archive()has a small legitimate option surface (format,prefix,worktree_attributes,remote, compression level, plus paths). Accepting those and rejecting the rest means a future git release cannot add another path-taking option that silently reopens this.--add-fileand--add-virtual-file, and make the membership rule "the option takes a filesystem path or URL" rather than "the option executes a command". The existing comment on--outputalready implies that rule; applying it consistently is what closes the class instead of this instance.Scope limits
Repo.archive(). That is the identical precondition to--output,--execand--template, all of which this project has treated as reportable.Disclosure
Reported privately via GitHub private vulnerability reporting. Happy to test a candidate patch against the PoC. No public disclosure until you have shipped a fix and are ready.
Addendum (2026-07-25) — related observation on the same membership question, filed here rather than separately
While auditing the archive denylist, the same class of gap was identified in unsafe_git_clone_options. A second advisory is not being requested, as the issue is lower severity and should inform the fix for the issue above rather than require separate triage. Recording it here to provide the complete picture in one place.
Repo._clone()treats a URL's protocol as a security boundary and appliescheck_unsafe_protocols()to exactly one input:git cloneaccepts a second URL via--bundle-uri=<uri>, which git dereferences before the main transport runs. That option is absent fromunsafe_git_clone_options, so the option guard passes it, andcheck_unsafe_protocols()never inspects it. A caller-influenced value therefore drives an outbound request from the host:Confirmed against a local listener — the request leaves the process:
file:///pathis likewise accepted without error. Note this is not a tokenisation bypass:multi_optionsisshlex.splitbefore the check (perc9a26789/ GHSA-x2qx-6953-8485), so the fully-split--bundle-uri=...token is checked and legitimately passes because the option is not on the list.Why it belongs with this report: both are the membership question rather than the matching logic — is the set of blocked options complete, and does the protocol guard inspect every URL git will dereference? The structural remediation proposed above covers both if extended slightly: prefer an allowlist per command, and route every URL-bearing option through
check_unsafe_protocols(), not only the positional URL. Adding--bundle-uritounsafe_git_clone_optionswould be the minimal fix.Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unguarded git read-tree option forwarding in IndexFile.from_tree/reset/merge_tree enables arbitrary file overwrite
GHSA-4gmw-gg2m-w46p
More information
Details
Summary
IndexFile.from_tree,IndexFile.reset(→ from_tree) andIndexFile.merge_treeappend caller-influenced treeish strings positionally togit read-treewith no unsafe-option guard, noallow_unsafe_optionsparameter, and no--separator.git read-tree --index-output=<file>writes the resulting index to an arbitrary path, and last-occurrence-wins lets an injected--index-outputoverride the method's internal temp path — clobbering an arbitrary file with a valid git-index blob. This is a distinct, never-guarded sink: commit3af0c251(GHSA-3f7w-8rr8-f37f) guarded onlycheckout_indexandtag;read_treewas left unprotected (it is among the acknowledged unguarded call sites in that advisory's sweep but was never reported or fixed).Root Cause
from_tree(index/base.py:388),reset(delegates to from_tree), andmerge_tree(index/base.py:291) callrepo.git.read_tree(*arg_list)with nocheck_unsafe_optionsand no--. The treeish is caller-influenced and positional.Impact
Arbitrary file overwrite / destruction at the privileges of the host process. Content is constrained to a git-index blob (not attacker-chosen, so not RCE), but the target path is fully attacker-controlled — corrupting/truncating configs or destroying files at attacker-chosen writable locations = I:H + A:H (per the skill's "overwrite-any-path = I:H" rule). Pure VALUE control (positional treeish). Default configuration.
Proof of Concept
Attack Chain
IndexFile.from_tree(repo, treeish)/reset(commit=…)/merge_tree(base=…, rhs=…)with attackertreeish="--index-output=/home/victim/.bashrc".allow_unsafe_optionsand never callcheck_unsafe_options.repo.git.read_tree(*arg_list)— no--. argv (from_tree, observed):['git','read-tree','--index-output=<tmp>','--index-output=/…/victim'](last-wins).Bypass Evidence
Independently reproduced (gate harness):
IndexFile.from_tree(repo,'--index-output=<victim>')→ victim overwritten; before=IMPORTANT ORIGINAL CONTENT, after startsDIRC\x00\x00\x00\x02…(destructive clobber, valid index blob).reset(commit=…)and bothmerge_treepositionals verified. Fix-commit read:3af0c251touched onlycheckout_index+tag;read_treeuntouched on HEAD.Affected Versions
GitPython <= 3.1.57(sinks present verbatim on the latest release tag).Suggested Fix
Add a
check_unsafe_optionsguard (with anallow_unsafe_optionsparameter) tofrom_tree/reset/merge_tree, and/or place a--separator before the positional treeish arguments; block--index-output(a path-taking option) on this sink.Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unguarded git option forwarding in Repo.init enables arbitrary command execution via --template clone hooks
GHSA-9rj7-rf2p-w77r
More information
Details
Summary
Repo.init()forwards**kwargsverbatim togit initwith no unsafe-option guard and noallow_unsafe_optionsparameter.git init --template=<dir>copies<dir>/hooks/*into the new repo's.git/hooks, so an attacker-controlledtemplatekwarg plants a hook that executes on the next git operation → arbitrary code execution.--templateis already recognized as unsafe for clone (it is onunsafe_git_clone_options, and GHSA-6p8h-3wgx-97gf covers the clone path), butRepo.initis a distinct method that never received a guard and needs an independent fix.Root Cause
Repo.init(path, mkdir, odbt, expand_vars, **kwargs)is a baregit.init(**kwargs)(git/repo/base.py:1435) with nocheck_unsafe_optionsand noallow_unsafe_options.Impact
Arbitrary code execution (hook fires on next git op) at the privileges of the host process. Two preconditions raise attack complexity (AC:H): the app must forward a
template=kwarg (KEY control) AND the attacker must stage an executable hook directory at a known path — the same profile GHSA-6p8h-3wgx-97gf accepted as HIGH for the clone path. Defaultallow_unsafe_optionsis irrelevant here becauseRepo.inithas no guard at all.Proof of Concept
Attack Chain
/evil/hooks/post-commit(executable) and gets the app to callRepo.init(path, template='/evil').Repo.init. Bypass proof: base.py:1435 is a baregit.init(**kwargs). argv (observed):['git','init','--template=/evil']./evil/hooks/post-commit→<repo>/.git/hooks/post-commit.Bypass Evidence
Independently reproduced (gate harness):
Repo.init(dst, template='<evil>')→ argv['git','init','--template=<evil>']unguarded; hook copied into.git/hooks/post-commit; aftergit committheINIT_ACEmarker was created.--separate-git-dir=<path>is a parallel arbitrary-redirect vector through the same unguarded sink (value control only).Affected Versions
GitPython <= 3.1.57(unguardedgit.init(**kwargs)present verbatim on the latest release tag).Suggested Fix
Add a
check_unsafe_optionsguard (with anallow_unsafe_optionsparameter) toRepo.init, consulting a denylist that includes--templateand--separate-git-dir(path-taking / hook-installing options).Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Arbitrary file read via --pathspec-from-file in IndexFile.remove() and Head.checkout()
GHSA-hh9p-6wh2-4mfc
More information
Details
Summary
IndexFile.remove()andHead.checkout()forward**kwargsintogit rmandgit checkoutwith no guard. Passing
--pathspec-from-file=<file>together with--pathspec-file-nulmakes Git treat the whole file as a single NUL-delimited pathspec, and the unmatched-pathspec
error quotes it verbatim. GitPython surfaces that through
GitCommandError.stderr, so theentire contents of a caller-chosen file are returned to the caller in band.
This is the same primitive as Instance 2 of
GHSA-3f7w-8rr8-f37f -
TagReference.create()with
-F, arbitrary file read returned in band - at two sites that advisory assessed andcleared.
Prior art, and why I am filing rather than commenting
GHSA-3f7w-8rr8-f37f's sweep table lists these four sites with the assessment
"
--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive found":IndexFile.remove()rm--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive foundIndexFile.move()mvHEAD.reset()resetHEAD.checkout()checkoutThat assessment is very nearly right, and I think that is why it held: with
--pathspec-from-filealone, Git splits on newlines and the error quotes only the firstline, which reads as an uninteresting partial. Adding
--pathspec-file-nul- a sibling flagof the same option, and the documented way to handle paths containing newlines - makes the
whole file one pathspec.
Root cause
git/index/base.py:991-1043:git/refs/head.py:237-268:Neither has an
allow_unsafe_optionsparameter or acheck_unsafe_options()call.Proof of concept
Observed on published 3.1.57, against a canary file holding three marked lines:
The two precision controls are there so the result is about these sinks and not about the
canary being visible everywhere.
Scope correction to the table above
Of the four sites cleared with that sentence, two disclose and two do not:
IndexFile.remove()→git rmHead.checkout()→git checkoutHEAD.reset()→git resetgit resetdoes not error on unmatched pathspecsIndexFile.move()→git mvThe two negatives are mentioned because "the dismissal was wrong" would overstate it: the
dismissal was wrong for half of what it covered.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Arbitrary Git Repository Creation Outside the Working Tree via Unvalidated .gitmodules Submodule Name in GitPython
GHSA-hmq2-w58f-27jc
More information
Details
Summary
GitPython computes the on-disk location of a submodule's separate Git directory (
.git/modules/<name>) from the submodule's.gitmodulessection name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g.../../../../home/victim/.something) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition is that a victim clones the malicious repository with GitPython and runs submodule initialization (submodule_update(init=True)/sm.update(init=True)), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython's independent reimplementation never adopted an equivalent check.Details
src/GitPython/git/objects/submodule/util.pysm_name()strips thesubmodule "/"wrapper from a.gitmodules[submodule "..."]header and returns the result unchecked.Submodule.iter_items()insrc/GitPython/git/objects/submodule/base.pyreads this viasm_name(sms)and assigns it tosm._name; unlike the submodulepath,nameis never used for a tree lookup, so it is never implicitly validated.Submodule._module_abspath()then buildsosp.join(parent_repo.git_dir, "modules", name)-os.path.joindoes not normalize../sequences.Submodule._clone_repo()passes this value straight toos.makedirs()and togit clone --separate-git-dir=<module_abspath>, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for.PoC
FROM python:3.11-slim, withgitinstalled viaapt-get install -y git(Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git2.34.1, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container viapip install /src/GitPythonfrom this repository's own source, which the advisory states resolved to the officially releasedGitPython==3.1.57andgitdb==4.0.12.repo.submodules+sm.update(init=True), equivalent togit submodule update --init).$ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc . $ docker run --rm ghsa-gitpython-poc(Per the Dockerfile,
docker runexecutes/work/run_all.sh, which in turn runsbuild_attacker_repo.sh, thenpoc_gitpython.py, thenpoc_control_realgit.sh.)4. Full source of the PoC script (
GHSA/testing/poc_gitpython.py), verbatim:.gitmodulessection header from[submodule "legit_dir"]to[submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"](built bybuild_attacker_repo.sh, part of the harness inGHSA/testing/). The malicious part is the../../../../../../traversal sequence embedded in the submodule name (not the tree-validatedpath), which becomes the on-disk target for the submodule's separate git directory.gitCLI control run) rejects the submodule name with "ignoring suspicious submodule name" and refuses to create anything outside the repository. GitPython instead created the escape-target directory and a fully-initialized Git repository at/tmp/gitpython_poc_escaped_root/modules_dir, confirmed byescape_target exists after update: Trueand its listed contents.url.Impact
Path traversal (CWE-22) / external control of file path (CWE-73) leading to arbitrary directory and Git-repository creation outside the intended clone directory. Integrity impact is High (attacker chooses destination path and, via the submodule URL, much of the written content); Confidentiality impact is None (only creation was demonstrated); Availability impact is Low-Medium (disk-exhaustion potential). No authentication is required; the attacker only needs to control a repository the victim clones and initializes submodules for - a routine, often fully-automatic operation in CI pipelines, IDE integrations, and dependency-management tooling.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: git-config OPTION-name injection via =/#/whitespace bypasses name validator, enabling forged core.sshCommand/hooksPath (RCE)
GHSA-jm78-9fvv-mhgr
More information
Details
Summary
GitPython's config-name validator only neutralizes CR/LF/NUL for the
"option"label; it does not reject=,#,;,[,], or whitespace in an option name.write_sectionwrites the option name verbatim into the config file, so an option name such assshCommand = touch <cmd> #is written as\tsshCommand = touch <cmd> # = <value>, which git parses ascore.sshCommand = touch <cmd>(the trailing#comments out the intended value). This forges arbitrary config directives (core.sshCommand,core.hooksPath,alias.*) → RCE on the next git operation. This is a distinct field (option name, not section name) and distinct character class (=/#/space, not newline/bracket) from GHSA-3rp5-jjmw-4wv2 (section-name bracket injection) and GHSA-mv93-w799-cj2w / GHSA-v87r-6q3f-2j67 (newline injection).Root Cause
_assure_config_name_safe(name, label)(git/config.py:897) applies the bracket/quote state machine ONLY whenlabel == "section"; for the"option"label it falls through with just theUNSAFE_CONFIG_CHARS_RE = [\r\n\x00]regex.write_sectionthen writes the option name verbatim into"\t%s = %s\n"(config.py:702).Impact
Arbitrary git-config directive injection → remote code execution via
core.sshCommand(fires on any ssh git operation, no staged file needed) orcore.hooksPath(with a staged hook). Requires the embedding application to forward a caller-influenced OPTION NAME into the config writer (name-control model, the same name-control model accepted by the related published advisories GHSA-3rp5-jjmw-4wv2 and GHSA-mv93-w799-cj2w). Default configuration.Proof of Concept
Attack Chain
set_value("core", "sshCommand = touch /tmp/RCE #", "x")._assure_config_name_safe(option, "option")@ config.py. Guard: regex matches only[\r\n\x00]; bracket/quote state machine is gated onlabel=="section". Bypass proof:=,#,space pass → noValueError.write_sectionwrites"\tsshCommand = touch /tmp/RCE # = x\n"(config.py:702).core.sshCommand=touch /tmp/RCE→ arbitrary code execution on next git op.Bypass Evidence
Independently reproduced (gate harness):
set_value('core','sshCommand = touch <RCE> #','x')→ noValueError; file linesshCommand = touch <RCE> # = x;git config --get core.sshCommand→touch <RCE>(rc=0). Also verifiedcore.hooksPathvia bothGitConfigParserandrepo.config_writer(). Fix-commit read: bracket/quote checks are insideif label == "section"; the"option"label is not covered.Affected Versions
GitPython <= 3.1.57(validator present verbatim on the latest release tag).Suggested Fix
Apply the section-name safety checks (reject
=,#,;,[,], whitespace) to the"option"label as well, or validate the fully-rendered config line after substitution.Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
GHSA-wvpp-8hx9-p66j
More information
Details
Summary
The
check_unsafe_optionsguard can be bypassed on every guarded method (clone/clone_from, fetch/pull/push, ls_remote, iter_commits, blame, archive) by combining a single-character kwarg withsplit_single_char_options=False. The guard's candidate list omits the smuggled option, buttransform_kwargemits a JOINED-n<value>argv token that git parses as--upload-pack=<cmd>, yielding arbitrary command execution at the defaultallow_unsafe_options=False. This is an incomplete-fix bypass of commite8d0fbf7(the fix for GHSA-r9mr-m37c-5fr3), which only emits value-derived candidates whensplit_single_char_optionsis True.Root Cause
_option_candidatesderives value-token candidates only underif len(key)==1 and split_single_char_options:(cmd.py:1048, added bye8d0fbf7). Withsplit_single_char_options=False,_option_candidates([], {"n":"utouch <cmd>;git-upload-pack"})returns only['-n'](not on the denylist), so the guard passes. Buttransform_kwarg('n', value, split_single_char_options=False)emits the JOINED token-nutouch <cmd>;git-upload-pack(cmd.py:1631). git clusters value-less short flags then parses-u<cmd>=--upload-pack=<cmd>→ command execution. The hardened guard WOULD block the joined token if it saw it — the flaw is it never receives it.Impact
Arbitrary OS command execution as the host process (via
--upload-pack) at defaultallow_unsafe_options=False, affecting all guarded methods that forward kwargs. Precondition: the app forwards a user-controlled kwargs dict containingsplit_single_char_options=Falseplus a single-char key (same user-dict-forwarding model GHSA-r9mr-m37c-5fr3 accepts).Proof of Concept
Attack Chain
Repo.clone_from(url, path, **kwargs):{split_single_char_options: False, n: 'utouch /tmp/ACE;git-upload-pack'}.check_unsafe_options(_option_candidates([], kwargs), unsafe_git_clone_options). Guard: denylist includes--upload-pack/-u. Bypass proof:_option_candidatesyields only['-n'](value token skipped becausesplit=False); guard never sees-u.transform_kwargemits joined token (cmd.py:1631). argv (observed):['git','clone','-v','-nutouch /tmp/ACE;git-upload-pack','--','<src>','<dst>'].-n+-u<cmd>→ runs upload-pack command → ACE.Bypass Evidence
Independently reproduced (gate harness, default
allow_unsafe_options=False): thesplit=Falsepayload created the markerVH05_GATE_ACE(ACE); the clone returned normally (guard bypassed). Control:n='--upload-pack=…'(split default True) →UnsafeOptionError: --upload-pack is not allowed. Fix-commit read:e8d0fbf7extends candidates only underif len(key)==1 and split_single_char_options:— split=False skips value emission. Also confirmed the earlier clustering-parse fix (commit56806080) does not cover this because the guard only ever receives['-n'].Affected Versions
GitPython <= 3.1.57(code present verbatim on the latest release tag).Suggested Fix
Make
_option_candidatesemit value-derived candidates regardless ofsplit_single_char_options(i.e. also for the joined-n<value>form), OR runcheck_unsafe_optionsover the fully-transformed argv rather than the reconstructed name-only candidate list.Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Release Notes
gitpython-developers/GitPython (gitpython)
v3.1.58: Security and FixesCompare Source
What's Changed
New Contributors
Full Changelog: gitpython-developers/GitPython@3.1.57...3.1.58
v3.1.57: - Security and FixesCompare Source
What's Changed
Configuration
📅 Schedule: (in timezone America/Costa_Rica)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.